package ru.space_game.core;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import ru.space_game.entities.*;
import ru.space_game.gui.GuiManager;
import ru.space_game.resource.*;
import ru.space_game.world.SpaceWorld;
import static org.lwjgl.opengl.GL11.*;

/**
 * The main hook of our game. This class with both act as a manager for the
 * display and central mediator for the game logic.
 * 
 * Display management will consist of a loop that cycles round all entities in
 * the game asking them to move and then drawing them in the appropriate place.
 * With the help of an inner class it will also allow the player to control the
 * main ship.
 * 
 * As a mediator it will be informed when entities within our game detect events
 * (e.g. alien killed, player died) and will take appropriate game actions.
 * 
 * <p>
 * NOTE:<br>
 * This game is a LWJGLized implementation of the Space Invaders game by Kevin
 * Glass. The original implementation is renderer agnostic and supports other
 * OpenGL implementations as well as Java2D. This version has been made specific
 * for LWJGL, and has added input control as well as sound (which the original
 * doesn't, at the time of writing). You can find the original article here:<br>
 * <a href="http://www.cokeandcode.com/"
 * target="_blank">http://www.cokeandcode.com</a>
 * </p>
 * 
 * @author Kevin Glass
 * @author Brian Matzon
 */
public class Game {

	/** The normal title of the window */
	private String WINDOW_TITLE = "INSERT GAME TITLE HERE";

	/** The width of the game display area */
	public static final int SCREEN_WIDTH = 800;

	/** The height of the game display area */
	public static final int SCREEN_HEIGHT = 600;

	/** Current instance of the Game */
	private static Game instance;

	/**
	 * The texture manager responsible for converting images into OpenGL
	 * textures
	 */
	private TextureManager textureManager;

	/** SoundManager to make sound with */
	private SoundManager soundManager;

	/** GUI manager to draw user interface */
	private GuiManager gui;

	/** Class with all game logic */
	private SpaceWorld world;

	/** True if we're holding up game play until a key has been pressed */
	private boolean waitingForKeyPress = true;

	/**
	 * The time at which the last rendering looped started from the point of
	 * view of the game logic
	 */
	private long lastLoopTime = getTime();

	/** True if the fire key has been released */
	private boolean fireHasBeenReleased;

	/** The time since the last record of fps */
	private long lastFpsTime;

	/** The recorded fps */
	private int fps;

	private static long timerTicksPerSecond = Sys.getTimerResolution();

	/** True if the game is currently "running", i.e. the game loop is looping */
	public static boolean gameRunning = true;

	/** Whether we're running in fullscreen mode */
	private boolean fullscreen;

	/** Mouse movement on x axis */
	private int mouseX;

	/** Mouse movement on y axis */
	private int mouseY;

	/** Is this an application or applet */
	private static boolean isApplication;

	private Sprite backgroundImage;

	private static final int FREQUENCY = 60;

	/**
	 * Construct our game and set it running.
	 * 
	 * @param fullscreen
	 * 
	 */
	public Game(boolean fullscreen) {
		this.fullscreen = fullscreen;
		initialize();
		instance = this;
		loadResources();
	}

	/**
	 * Get the high resolution time in milliseconds
	 * 
	 * @return The high resolution time in milliseconds
	 */
	public static long getTime() {
		// we get the "timer ticks" from the high resolution timer
		// multiply by 1000 so our end result is in milliseconds
		// then divide by the number of ticks in a second giving
		// us a nice clear time in milliseconds
		return (Sys.getTime() * 1000) / timerTicksPerSecond;
	}

	public static Game getInstance() {
		return instance;
	}

	/**
	 * Sleep for a fixed number of milliseconds.
	 * 
	 * @param duration
	 *            The amount of time in milliseconds to sleep for
	 */
	public static void sleep(long duration) {
		try {
			Thread.sleep((duration * timerTicksPerSecond) / 1000);
		} catch (InterruptedException inte) {
		}
	}

	/**
	 * Initialize the common elements for the game
	 */
	private void initialize() {
		// initialize the window beforehand
		try {
			setDisplayMode();
			Display.setTitle(WINDOW_TITLE);
			Display.setFullscreen(fullscreen);
			Display.create();

			// grab the mouse, dont want that hideous cursor when we're playing!
			if (isApplication) {
				Mouse.setGrabbed(true);
			}

			// enable textures since we're going to use these for our sprites
			glEnable(GL_TEXTURE_2D);

			// disable the OpenGL depth test since we're rendering 2D graphics
			glDisable(GL_DEPTH_TEST);

			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();

			glOrtho(0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, -1, 1);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

			// This will enable the standard type of blending
			// We do not have to use this every render call, only once at the
			// start
			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

			textureManager = new TextureManager();

			world = new SpaceWorld();

			// create our sound manager, and initialize it with 7 channels
			// 1 channel for sounds, 6 for effects - this should be enough
			// since we have a most 4 shots on screen at any one time, which
			// leaves
			// us with 2 channels for explosions.
			soundManager = new SoundManager();
			soundManager.initialize(8);

		} catch (LWJGLException le) {
			System.out.println("Game exiting - exception in initialization:");
			le.printStackTrace();
			Game.gameRunning = false;
			return;
		}

	}

	private void loadResources() {
		// TODO move sounds to soundManager or create new class Sounds
		// load our sound data
		Sounds.init();

		backgroundImage = getSprite("background1.gif");

		gui = new GuiManager();
		gui.init();

		// setup the initial game state
		startGame();

	}

	/**
	 * Sets the display mode for fullscreen mode
	 */
	private boolean setDisplayMode() {
		try {
			// get modes
			DisplayMode[] dm = org.lwjgl.util.Display.getAvailableDisplayModes(
					SCREEN_WIDTH, SCREEN_HEIGHT, -1, -1, -1, -1, 60, 60);

			org.lwjgl.util.Display.setDisplayMode(dm, new String[] {
					"width=" + SCREEN_WIDTH,
					"height=" + SCREEN_HEIGHT,
					"freq=" + FREQUENCY,
					"bpp="
							+ org.lwjgl.opengl.Display.getDisplayMode()
									.getBitsPerPixel() });
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			System.out
					.println("Unable to enter fullscreen, continuing in windowed mode");
		}

		return false;
	}

	/**
	 * Start a fresh game, this should clear out any old data and create a new
	 * set.
	 */
	private void startGame() {
		// clear out any existing entities and initialize a new set
		world.clearEntities();
		world.initEntities();
		gui.getAnnouncer().hideAllMessages();
	}

	/**
	 * Notification that the player has died.
	 */
	public void notifyDeath() {
		if (!waitingForKeyPress) {
			soundManager.playSound(Sounds.SOUND_LOOSE);
		}
		gui.getAnnouncer().showMessage("youLose");
		waitingForKeyPress = true;
	}

	/**
	 * Notification that the player has won since all the aliens are dead.
	 */
	public void notifyWin() {
		gui.getAnnouncer().showMessage("youWin");
		waitingForKeyPress = true;
		soundManager.playSound(Sounds.SOUND_WIN);
	}

	/**
	 * Run the main game loop. This method keeps rendering the scene and
	 * requesting that the callback update its screen.
	 */
	private void gameLoop() {
		while (Game.gameRunning) {
			// clear screen
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();

			// let subsystem paint
			frameRendering();

			// update window contents
			Display.update();
		}

		// clean up
		soundManager.destroy();
		Display.destroy();
	}

	/**
	 * Notification that a frame is being rendered. Responsible for running game
	 * logic and rendering the scene.
	 */
	public void frameRendering() {
		// SystemTimer.sleep(lastLoopTime+10-SystemTimer.getTime());
		Display.sync(240);

		// work out how long its been since the last update, this
		// will be used to calculate how far the entities should
		// move this loop
		long delta = getTime() - lastLoopTime;
		lastLoopTime = getTime();
		lastFpsTime += delta;
		fps++;

		// update our FPS counter if a second has passed
		if (lastFpsTime >= 1000) {
			Display.setTitle(WINDOW_TITLE + " (FPS: " + fps + ")" + " (X: "
					+ world.getShip().getX() + "; Y: " + world.getShip().getY()
					+ ")");
			lastFpsTime = 0;
			fps = 0;
		}

		// draw background image
		backgroundImage.draw(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0);

		world.moveEntities(delta);

		world.processCollisions();

		world.drawEntities();

		gui.draw();

		world.doLogic();

		// if we're waiting for an "any key" press then draw the
		// current message
		if (waitingForKeyPress)
			gui.getAnnouncer().showMessage("pressAnyKey");
		else
			gui.getAnnouncer().hideMessage("pressAnyKey");

		ShipEntity ship = world.getShip();

		// resolve the movement of the ship. First assume the ship
		// isn't moving. If either cursor key is pressed then
		// update the movement appropriately
		ship.setSpeed(0);

		// get mouse movement on x axis. We need to get it now, since
		// we can only call getDX ONCE! - secondary calls will yield 0, since
		// there haven't been any movement since last call.
		mouseX = Mouse.getDX();
		mouseY = Mouse.getDY();

		// we delegate input checking to submethod since we want to check
		// for keyboard, mouse & controller
		boolean leftPressed = hasInput(Keyboard.KEY_LEFT);
		boolean rightPressed = hasInput(Keyboard.KEY_RIGHT);
		boolean upPressed = hasInput(Keyboard.KEY_UP);
		boolean downPressed = hasInput(Keyboard.KEY_DOWN);
		boolean firePressed = hasInput(Keyboard.KEY_SPACE);

		if (!waitingForKeyPress && !soundManager.isPlayingSound()) {
			if ((leftPressed) && (!rightPressed)) {
				ship.setAngel((ship.getAngel() + 1) % 360);
			} else if ((rightPressed) && (!leftPressed)) {
				ship.setAngel((ship.getAngel() - 1) % 360);
			}

			if ((upPressed) && (!downPressed)) {
				ship.setSpeed(-ship.getSpeed() - 200);
			} else if ((downPressed) && (!upPressed)) {
				ship.setSpeed(ship.getSpeed() + 200);
			}

			// if we're pressing fire, attempt to fire
			if (firePressed && ship.canFire()) {
				ship.fire();
			}
		} else {
			if (!firePressed) {
				fireHasBeenReleased = true;
			}
			if ((firePressed) && (fireHasBeenReleased)
					&& !soundManager.isPlayingSound()) {
				waitingForKeyPress = false;
				fireHasBeenReleased = false;
				startGame();
				soundManager.playSound(Sounds.SOUND_START);
			}
		}

		// if escape has been pressed, stop the game
		if ((Display.isCloseRequested() || Keyboard
				.isKeyDown(Keyboard.KEY_ESCAPE)) && isApplication) {
			Game.gameRunning = false;
		}
	}

	/**
	 * @param direction
	 * @return
	 */
	private boolean hasInput(int key) {
		switch (key) {
		case Keyboard.KEY_LEFT:
			return Keyboard.isKeyDown(Keyboard.KEY_LEFT) || mouseX < 0;

		case Keyboard.KEY_RIGHT:
			return Keyboard.isKeyDown(Keyboard.KEY_RIGHT) || mouseX > 0;

		case Keyboard.KEY_UP:
			return Keyboard.isKeyDown(Keyboard.KEY_UP) || mouseY > 0;

		case Keyboard.KEY_DOWN:
			return Keyboard.isKeyDown(Keyboard.KEY_DOWN) || mouseY < 0;

		case Keyboard.KEY_SPACE:
			return Keyboard.isKeyDown(Keyboard.KEY_SPACE)
					|| Mouse.isButtonDown(0);
		}
		return false;
	}

	/**
	 * The entry point into the game. We'll simply create an instance of class
	 * which will start the display and game loop.
	 * 
	 * @param argv
	 *            The arguments that are passed into our game
	 */
	public static void main(String argv[]) {
		isApplication = true;
		System.out.println("Use -fullscreen for fullscreen mode");
		new Game((argv.length > 0 && "-fullscreen".equalsIgnoreCase(argv[0])))
				.execute();
		System.exit(0);
	}

	/**
	 *
	 */
	public void execute() {
		gameLoop();
	}

	/**
	 * Create or get a sprite which displays the image that is pointed to in the
	 * classpath by "ref"
	 * 
	 * @param ref
	 *            A reference to the image to load
	 * @return A sprite that can be drawn onto the current graphics context.
	 */
	public Sprite getSprite(String ref) {
		return new Sprite(textureManager, ref);
	}

	public SoundManager getSoundManager() {
		return soundManager;
	}

	public SpaceWorld getWorld() {
		return world;
	}

	// TODO think about game states.
	// Waiting for key press will not be the only one state.
	public boolean isWaitingForKeyPress() {
		return waitingForKeyPress;
	}

}